﻿// some constants to use for the layout, based on the width of the current window
var stepWidth,
    middleColumnGuide,
    leftColumnGuide,
    rightColumnGuide,
    stepSpacer,
    serviceUrl;

$(document).ready(function () {
    // hide the spinner, settings and the flowWorkspace, if showing
    $('#spinner').hide();
    $('#flowWorkspace').hide();
    $('#settingsDialog').hide();

    // initialise the settings dialog
    setupSettingsDialog();
});

$(window).resize(function () {
    if (getCookie("resizeRefresh") === "on") {
        $('#spinner').show();
        drawPage();
    }
});

function setupSettingsDialog() {
    // set cookie, if not set yet
    if (document.cookie.length > 0) {
        if (getCookie("timeWindow") === "rolling") {
            $("#windowRolling")[0].checked = true;
        } else {
            $("#windowSpecific")[0].checked = true;
        }
        if (getCookie("useDemoData") === "on") {
            $("input[name='chkDemoData']").prop("checked", true);
        } else {
            $("input[name='chkDemoData']").prop("checked", false);
        }
        if (getCookie("resizeRefresh") === "on") {
            $("input[name='chkResizeRefresh']").prop("checked", true);
        } else {
            $("input[name='chkResizeRefresh']").prop("checked", false);
        }
        $("#lastMinutes").val(getCookie("lastMinutes"));
        $("#startTime").val(getCookie("startTime"));
        $("#endTime").val(getCookie("endTime"));
    } else {
        // default to values if no cookie set
        $("#windowRolling")[0].checked = true;
        $("#lastMinutes").val(10);
        var startTime = new Date(new Date().getTime() - 12 * 60000);
        var endTime = new Date(new Date().getTime() - 2 * 60000);
        $("#startTime").val($.datepicker.formatDate("yy/mm/dd", startTime) + " " + startTime.getHours() + ":" + startTime.getMinutes());
        $("#endTime").val($.datepicker.formatDate("yy/mm/dd", endTime) + " " + endTime.getHours() + ":" + endTime.getMinutes());
    }

    // initialise the settings dialog using jQuery UI
    $("#settingsDialog").dialog({
        autoOpen: false,
        height: 480,
        width: 400,
        modal: true,
        resizable: false,
        buttons: {
            Save: function () {
                // save settings to cookies
                setCookie("timeWindow", $("input[name='radTimeWindow']:checked").val(), 100);
                setCookie("lastMinutes", $("#lastMinutes").val(), 100);
                setCookie("startTime", $("#startTime").val(), 100);
                setCookie("endTime", $("#endTime").val(), 100);
                setCookie("useDemoData", $("input[name='chkDemoData']:checked").val(), 100);
                setCookie("resizeRefresh", $("input[name='chkResizeRefresh']:checked").val(), 100);

                // close the dialog
                $(this).dialog("close");

                // show the loading image
                $('#spinner').show();

                // refresh the page
                drawPage();
            }
        }
    });

    // initialise the input spinner
    $('#lastMinutes').spinner();

    // initialise date time pickers
    var currentDateTime = new Date();
    $('#startTime').datetimepicker({
        timeFormat: "HH:mm",
        dateFormat: "yy/mm/dd"
    });
    $('#endTime').datetimepicker({
        timeFormat: "HH:mm",
        dateFormat: "yy/mm/dd"
    });

    // make input fields match the jQuery UI theme
    $('.textInput')
      .button()
      .css({
          'font': 'inherit',
          'color': 'inherit',
          'text-align': 'left',
          'outline': 'none',
          'cursor': 'text'
      });
}

// set a cookie value
function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
    var expires = "expires=" + d.toGMTString();
    document.cookie = cname + "=" + cvalue + "; " + expires;
}

// get a cookie value
function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name) === 0) { return c.substring(name.length, c.length); }
    }
    return "";
}

function drawPage() {
    // clear the workspace and show, if not already showing & hide the welcome text
    $('#flowWorkspace').html('');
    $('#flowWorkspace').show();
    $('#welcomeText').hide();

    // some settings to use for the layout, based on the width of the current window
    var windowWidth = $(window).width() - 18; // adjust slightly, to compensate for the scroll bar
    if (windowWidth > 1100) {
        stepWidth = windowWidth / 6;
        leftColumnGuide = (windowWidth / 2) - (stepWidth / 2) - (2 * stepWidth);
    } else { // keep a minimum size
        stepWidth = 200; 
        leftColumnGuide = 10; // don't set this to zero, as it will mess with later calculations
    }
    middleColumnGuide = (stepWidth * 2) + leftColumnGuide;
    rightColumnGuide = (stepWidth * 4) + leftColumnGuide;
    stepSpacer = 110;

    // set the flowWorkspace height to adjust for nav
    $(".flowWorkspace").css({ height: $(window).height() - 50 + "px" });

    // setup some defaults for jsPlumb.
    var instance = jsPlumb.getInstance({
        Anchors: ["Right", "Left"],
        Endpoint: "Blank",
        Connector: ["Straight"],
        ConnectionOverlays: [
            ["Arrow", {
                width: 10,
                location: 1,
                id: "arrow",
                length: 10,
                foldback: 0.8
            }]
        ],
        Container: "flowWorkspace"
    });

    // suspend drawing and create links.
    instance.doWhileSuspended(function () {
        // prepare the JSON request variables
        var startDate,
            endDate,
            timezoneOffset,
            jsonURL,
            currentDateTime;

        // get the start and end date and timezone offset
        if (getCookie("timeWindow") === "specific") {
            startDate = $("#startTime").datetimepicker("getDate");
            endDate = $("#endTime").datetimepicker("getDate");
        } else {
            startDate = new Date(new Date().getTime() - 10 * 60000);
            endDate = new Date(new Date().getTime());
        }
        timezoneOffset = new Date().getTimezoneOffset();

        // build the JSON URL
        currentDateTime = new Date();
        // use demo data or real data?
        if ($("input[name='chkDemoData']:checked").val() === "on") {
            // use sample data
            jsonURL = "scripts/sampleData.js";

            RenderFlowData(jsonURL, instance);
        } else {
            // use real data
            // get the flow data service URL
            $.getJSON("ConfigService.svc/JSON/getSetting/flowDataUrl")
                .fail(function (jqxhr, textStatus, error) {
                    var err = textStatus + ", " + error;
                    $("<div></div>").dialog({
                        buttons: { "Ok": function () { $(this).dialog("close"); } },
                        close: function (event, ui) { $(this).remove(); },
                        resizable: false,
                        title: "Error",
                        modal: true
                    }).text("Error occurred while retrieving configuration setting for flow data URL, trying local flow data service: " + err);
                    serviceURL = "FlowData.svc";
                })
                .done(function (flowUrl) {
                    serviceURL = flowUrl;

                    // build up the flow data request, passing the timestamps as number of millisecons since Jan 1, 1970
                    jsonURL = serviceURL + "/JSON/getFlowData/{startDate}/{endDate}/{timezoneOffset}?{currentDateTime}";
                    jsonURL = jsonURL.replace("{startDate}", Date.parse(startDate.toISOString()));
                    jsonURL = jsonURL.replace("{endDate}", Date.parse(endDate.toISOString()));
                    jsonURL = jsonURL.replace("{currentDateTime}", Date.parse(currentDateTime.toISOString()));
                    jsonURL = jsonURL.replace("{timezoneOffset}", escape(timezoneOffset.toString()));

                    RenderFlowData(jsonURL, instance);
                });
        }
    });
}

function RenderFlowData(jsonURL, instance) {
    // get the JSON formatted data for steps and links
    $.getJSON(jsonURL)
        .fail(function (jqxhr, textStatus, error) {
            var err;
            if (jqxhr.statusText === "Error: The system cannot locate the resource specified.\r\n") {
                err = "No flow data service found, please check the web.config file";
            } else if (jqxhr.responseJSON != null) {
                err = error + ": \r\n" + jqxhr.responseJSON;
            } else {
                err = "Infrastructure error: \r\n" + jqxhr.responseText;
            }
            $("<div></div>").dialog({
                buttons: { "Ok": function () { $(this).dialog("close"); } },
                close: function (event, ui) { $(this).remove(); },
                resizable: false,
                title: "Error",
                modal: true
            }).text("Error occurred while retrieving data: \r\n" + err);
        })
        .done(function (visualizationData) {
            // show a spinner, if not already showing
            $("#spinner").show();

            // get the two sets of data
            var linksToShow = visualizationData.links,
                orchStepsToShow = visualizationData.orchSteps,
                portsToShow = visualizationData.ports,
                displayOrder = visualizationData.displayOrder;

            if (linksToShow.length === 0) {
                $("<div></div>").dialog({
                    buttons: { "Ok": function () { $(this).dialog("close"); } },
                    close: function (event, ui) { $(this).remove(); },
                    resizable: false,
                    title: "Info Alert",
                    modal: true
                }).text("No data found");
            } else {
                // create the relevant shapes for display
                createPortShapes(portsToShow);
                createOrchShapes(orchStepsToShow);

                // position the steps
                positionSteps(instance, displayOrder);

                // create orchestration boxes
                createOrchestrationBoxes(orchStepsToShow);

                // link the steps
                linkSteps(instance, linksToShow);
            }
        })
        .always(function () {
            // hide the spinner
            $("#spinner").hide();
        });
}

// Create the shapes that will represent receive and send ports
function createPortShapes(portsToShow, linksToShow) {
    // cycle through each port that needs to be created
    for (var portIndex = 0; portIndex < portsToShow.length; portIndex++) {
        // create the port shape
        var port = portsToShow[portIndex];
        if (port.PortType === "Error") {
            createStepShape("stepShape errorShape", port.PortName, port.ShapeID, port.ErrorMessageCount, -1, 0);
        } else {
            createStepShape("stepShape", port.PortName, port.ShapeID, port.ReceiveMessageCount, port.AvgMilliSeconds, 0);
        }
    }
}

// Create the shapes that will represent orchestration receive and send steps
function createOrchShapes(orchStepsToShow) {
    // cycle through each step that needs to be created
    for (var orchIndex = 0; orchIndex < orchStepsToShow.length; orchIndex++) {
        // create the orch step shape
        var orchStep = orchStepsToShow[orchIndex];
        createStepShape("stepShape orchShape", orchStep.StepName, orchStep.StepID, orchStep.MessageCount, -1, orchStep.OrchID);
    }
}

// create a step shape
function createStepShape(cssClasses, stepName, shapeID, messageCount, avgMilliseconds, orchID) {
    // build up the template
    var stepHTML = "<div class='{css}' style='width:{stepWidth}px; max-width:{stepWidth}px' title='{Name}' id='{ID}' orchID='{OrchID}'>" +
        "<div class='stepTitle'>{Name}</div>" +
        "<div class='infoCount'>{MessageCount}</div>";

    // only show the processing time if it is not -1
    if (avgMilliseconds > -1) {
        stepHTML += "<div class='infoTime'>{time}</div>";
    }

    // close it up
    stepHTML += "</div>";

    // replace the placeholders, using globale regular expression identifiers to ensure all instances are replaced
    stepHTML = stepHTML.replace(/{css}/g, cssClasses);
    stepHTML = stepHTML.replace(/{stepWidth}/g, stepWidth);
    stepHTML = stepHTML.replace(/{Name}/g, stepName);
    stepHTML = stepHTML.replace(/{ID}/g, shapeID);
    stepHTML = stepHTML.replace(/{OrchID}/g, orchID);
    stepHTML = stepHTML.replace(/{MessageCount}/g, messageCount);
    stepHTML = stepHTML.replace(/{time}/g, avgMilliseconds);

    // add to the page
    $("#flowWorkspace").append(stepHTML);

}

// position the created steps, using the order of items in the stepsToShow array
function positionSteps(instance, displayOrder) {
    // intialise variables and defaults
    var runningTopIndex = 50, // starting point for the first shape, and the tracking index to position subsequent shapes
        currentTopIndex = 0,
        arrPosTracking = []; // array to keep track of where steps have been positioned

    // cycle through the sorted data, positioning the divs sensibly
    for (var displayIndex = 0; displayIndex < displayOrder.length; displayIndex++) {
        var currentStep = displayOrder[displayIndex],
            leftIndex = 0,
            thisShape = $("#" + currentStep.ID),
            errorShape = $("#" + currentStep.ID + "-Error"),
            topIndex = runningTopIndex;

        if (thisShape.length === 0) {
            alert(currentStep.ID + " step does not exist");
        } else {
            // if the current shape has not yet been positioned, work out where it needs to be, and then move it
            if (thisShape.position().left === 0) {
                // left index position is determined by the type of shape we are placing
                if (currentStep.Type === "Rcv") {
                    leftIndex = leftColumnGuide;
                } else if (currentStep.Type === "Orc") {
                    leftIndex = middleColumnGuide;
                } else { // errors and send ports 
                    leftIndex = rightColumnGuide;
                }
            }

            // if necessary, move the current shape down until it does not overlap with any other shape
            while (arrPosTracking[topIndex + '-' + leftIndex] !== undefined) {
                topIndex += stepSpacer;
            }

            // may also need to move it down if there is a shape already to the right of it (either in the middle or right columns)
            if (leftIndex === leftColumnGuide) {
                while (arrPosTracking[topIndex + '-' + middleColumnGuide] !== undefined) {
                    topIndex += stepSpacer;
                }
            }
            if (leftIndex === leftColumnGuide || leftIndex === middleColumnGuide) {
                while (arrPosTracking[topIndex + '-' + rightColumnGuide] !== undefined) {
                    topIndex += stepSpacer;
                }
            }

            // position the shape accordingly
            thisShape.css({ top: topIndex + 'px', left: leftIndex + 'px' });
            arrPosTracking[topIndex + '-' + leftIndex] = true;

            // if there is a related error shape, position it to the right, moving it down if necessary
            if (errorShape.length > 0) {
                while (arrPosTracking[topIndex + '-' + rightColumnGuide] !== undefined) {
                    topIndex += stepSpacer;
                }
                errorShape.css({ top: topIndex + 'px', left: rightColumnGuide + 'px' });
                arrPosTracking[topIndex + '-' + rightColumnGuide] = true;
            }

            // update the running top index
            runningTopIndex = topIndex;
        }
    }

    // add an empty spacer at the end, if necessary
    if ($("#flowWorkspace").outerHeight() < runningTopIndex + 50) {
        $("#flowWorkspace").append("<div style='height:75px;position:absolute;left:0px;top:" +
            runningTopIndex + "px'>&nbsp;</div>");
    }
}

// for each unique orchestration ID, find the related steps and then create a div "around" them
function createOrchestrationBoxes(orchStepsToShow) {
    // keep track of which orchestrations have already been dealt with
    var orchBoxIDs = [];

    // cycle through each orchID
    for (var orchStepIndex = 0; orchStepIndex < orchStepsToShow.length; orchStepIndex++) {
        // initialise variables
        var currentOrchStep = orchStepsToShow[orchStepIndex],
            orchPadding = 15,
            topIndex = -1,
            botIndex = -1,
            maxWidth = -1;

        // if we have not yet built a box for this orchID
        if (orchBoxIDs[currentOrchStep.OrchID] === undefined) {
            // find all shapes with the current orchID as group ID
            var shapesInGroup = $(".orchShape[orchID='" + currentOrchStep.OrchID + "']");
            for (var shapeIndex = 0; shapeIndex < shapesInGroup.length; shapeIndex++) {
                // update the top and bottom index holders
                var thisShape = $("#" + shapesInGroup[shapeIndex].id),
                    thisTop = thisShape.position().top,
                    thisBot = thisTop + thisShape.outerHeight(),
                    thisWidth = thisShape.outerWidth();
                if (topIndex === -1 || thisTop < topIndex) {
                    topIndex = thisTop;
                }
                if (botIndex === -1 || thisBot > botIndex) {
                    botIndex = thisBot;
                }
                if (maxWidth === -1 || thisWidth > maxWidth) {
                    maxWidth = thisWidth;
                }
            }

            // create the orchestration box
            // build the box HTML and add to workspace
            var orchBox = jQuery('<div/>', {
                "class": "orchBox",
                id: currentOrchStep.OrchID,
                title: currentOrchStep.OrchName,
                css: {
                    top: topIndex - orchPadding + 'px', // add extra padding to the top for the orch title
                    left: middleColumnGuide - orchPadding + 'px',
                    height: botIndex - topIndex + (2 * orchPadding) + 'px',
                    width: maxWidth + (2 * orchPadding) + 'px',
                }
            });
            orchBox.append("<div class='orchTitle' " +
                "style='width: " + (maxWidth + (2 * orchPadding) - 10) + "px; " +
                "max-width: " + (maxWidth + (2 * orchPadding) - 10) + "px'" +
                ">" + currentOrchStep.OrchName + "</div>");
            $('#flowWorkspace').append(orchBox);

            // position the orchTitle, based on the height it takes up 
            var orchTitleHeight = $('#' + currentOrchStep.OrchID + '>.orchTitle').outerHeight() - 1;
            $('#' + currentOrchStep.OrchID + '>.orchTitle').css({
                top: -1 * orchTitleHeight
            });

            // add the current orchID to the array
            orchBoxIDs[currentOrchStep.OrchID] = true;
        }
    }
}

// connect the steps together
function linkSteps(instance, linksToShow) {
    // cycle through the links, determine the parameters for the link, and then connect them
    for (var linkIndex = 0; linkIndex < linksToShow.length; linkIndex++) {
        // initialise and set defaults
        var currentLink = linksToShow[linkIndex],
            linkAnchors = ["Right", "Left"],
            linkConnector = ["Straight"],
            linkCSS = "yellowLine",
            timeLabelCSS = "timeLabel"
            countLocation = 0.65,
            timeLocation = 0.35;

        // if the from and to shapes exist, link them, else raise an error window
        if ($("#" + currentLink.FromShapeID).length > 0 && $("#" + currentLink.ToShapeID).length > 0) {
            // where the defaults above are not applicable, define the link paramters, based on the type of connection
            if (currentLink.FromType === "Orc" && currentLink.ToType === "Orc") { // one orchestration step to another in the same orchestration
                linkAnchors = ["Bottom", "Top"];
            } else if (currentLink.FromType === "Orc" && currentLink.ToType === "New Orchestration") { //one orchestration step to another in a different orchestration
                linkConnector = ["Flowchart", { stub: 50, cornerRadius: 10, midpoint: 0.4 }];
            } else if (currentLink.FromType === "Rcv" && currentLink.ToType === "Snd") { //receive port to send port
                linkCSS += " messagingLine"; // add CSS style to place line behind any orchestration shapes
                timeLocation = 0.78;
                countLocation = 0.89;
            } else if (currentLink.FromType === "Snd" && currentLink.ToType === "Rcv") { // two-way send
                linkAnchors = ["Bottom", "Top"];
                linkCSS += " dottedLine";
                linkConnector = ["Bezier"];
            } else if (currentLink.FromType === "Orc" && currentLink.ToType === "Rcv") { // response from an orc via a two-way receive
                linkAnchors = ["Left", "Bottom"];
                linkCSS += " dottedLine";
                linkConnector = ["Bezier"];
            } else if (currentLink.LinkType === "Error") { // if it is an error link, we have a little more work to do
                linkCSS = "redLine";
                timeLabelCSS = "hiddenLabel"; // hide the time label
                // if we are link a send port error, we need a different connector
                if ($("#" + currentLink.FromShapeID).position().left === rightColumnGuide) {
                    linkAnchors = ["Bottom", "Top"];
                } else { // for a receive port link, we change the location of the labels
                    timeLocation = 0.78;
                    countLocation = 0.89;
                }
            }

            // create the link
            instance.connect({
                source: currentLink.FromShapeID,
                target: currentLink.ToShapeID,
                anchors: linkAnchors,
                connector: linkConnector,
                cssClass: linkCSS,
                overlays: [
                    ["Label", {
                        label: currentLink.MessageCount.toString(),
                        id: "labelCount",
                        cssClass: "countLabel",
                        location: countLocation
                    }],
                    ["Label", {
                        label: currentLink.AvgMilliSeconds.toString(),
                        id: "timeLabel",
                        cssClass: timeLabelCSS,
                        location: timeLocation
                    }]
                ]
            });
        } else {
            // show alert
            alert("shape id " + currentLink.FromShapeID + " or shape id " + currentLink.ToShapeID + " does not exist");
        }
    }
}

function endsWith(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
}